<?php

declare(strict_types=1);

namespace think\auth\service;

use think\App;
use DateTimeZone;
use DateTimeImmutable;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
use think\auth\struct\TokenConfig;
use Lcobucci\JWT\Token as JwtToken;
use Lcobucci\JWT\Validation\Constraint\ValidAt;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use think\exception\HttpException;
use think\facade\Log;
use function date_default_timezone_get;

class Token
{
    /**
     * @var array
     */
    protected array $claims;

    /**
     * @var JwtToken
     */
    protected JwtToken $token;

    /**
     * @var Configuration
     */
    private Configuration $jwtConfiguration;

    /**
     * @var App
     */
    private App $app;
    /**
     * @var mixed|null
     */
    private mixed $options;

    public function __construct(App $app)
    {
        $this->app = $app;
        $this->options = new TokenConfig($app->config->get("auth.token", []));

        $this->_initJwtConfiguration();
    }

    /**
     * 创建Token
     * @param int|string $identifier 唯一凭据，如用户ID、UUID等
     * @param array $claims
     * @return string
     */
    public function Create(int|string $identifier, array $claims = []): string
    {
        $now = new DateTimeImmutable();
        $builder = $this->jwtConfiguration->builder()
            ->permittedFor($this->options->getAud())
            ->issuedBy($this->options->getIss())
            ->identifiedBy((string)$identifier)
            ->issuedAt($now)
            ->canOnlyBeUsedAfter($now)
            ->expiresAt($this->getExpiryDateTime($now))
            ->relatedTo((string)$identifier);

        foreach ($claims as $key => $value) {
            $builder->withClaim($key, $value);
        }
        $token = $builder->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey())->toString();
        return $this->_encryptToken($token);
    }

    /**
     * 验证 Token
     * @param string|null $token
     * @return bool
     */
    public function Verify(string $token = null): bool
    {
        if(!empty($token)) {
            $token = $this->Parse($token);
        } else {
            $token = $this->Fetch();
        }

        if (!$token) throw new HttpException(401, "登录凭据无效");
        if ($this->_isInvalid($token)) throw new HttpException(401, "登录凭据无效");
        if ($this->_isExpired($token)) throw new HttpException(403, "登录已过期，请重新登录");
        return true;
    }


    /**
     * 从Request中取得JwtToken
     * @param string $handle
     * @return null|JwtToken
     */
    public function Fetch(string $handle = ''): null|JwtToken
    {
        $handle = $handle ?: $this->options->getType();
        $handles = str_contains($handle, '|') ? explode('|', $handle) : [trim($handle)];
        $token = null;
        foreach ($handles as $handle) {
            $tokenText = $this->_fetchToken($handle);
            if (!empty($tokenText) && is_string($tokenText)) {
                $token = $tokenText;
                break;
            }
        }

        if (!$token) return null;
        return $this->Parse($token);
    }

    /**
     * 将字符串token还原为JwtToken
     * @param string $token 字符串token
     */
    public function Parse(string $token): JwtToken|null
    {
        try {
            $token = $this->_decryptToken($token);
            if(!$token) return null;
            return $this->jwtConfiguration->parser()->parse($token);
        } catch (\Exception $e) {
            Log::error("Token->Parse： {$e->getMessage()}");
            return null;
        }
    }

    /**
     * Token续期
     * @param JwtToken $token
     * @return string
     */
    public function Renewal(JwtToken $token): string
    {
        $claims = $token->claims()->all();

        $jti = $claims['jti'];
        unset($claims['aud']);
        unset($claims['iss']);
        unset($claims['jti']);
        unset($claims['iat']);
        unset($claims['nbf']);
        unset($claims['exp']);
        unset($claims['sub']);

        $token = $this->Create($jti, $claims);
        $refreshAt = $this->options->getRefreshTTL();

        header('Access-Control-Expose-Headers:Automatic-Renewal-Token,Automatic-Renewal-Token-RefreshAt');
        header("Automatic-Renewal-Token:" . $token);
        header("Automatic-Renewal-Token-RefreshAt:$refreshAt");

        return $token;
    }

    /**
     * 作废Token
     * @param string $token
     */
    public function Destroy(string $token)
    {
        $jwtToken = $this->Parse($token);
        if(!empty($jwtToken)) {
            $exp = $jwtToken->claims()->get('exp');
            $ttl = $this->options->getRefreshTTL();
            $exp->modify("-{$ttl} sec");
        }
    }

    public function Options(): TokenConfig
    {
        return $this->options;
    }

    private function _initJwtConfiguration()
    {
        $this->jwtConfiguration = Configuration::forSymmetricSigner(
            $this->options->getSigner(),
            $this->options->getSignerKey()
        );
    }

    private function getExpiryDateTime($now): DateTimeImmutable
    {
        $ttl = (string)$this->options->getExpires();
        return $now->modify("+{$ttl} sec");
    }

    private function getValidateConfig(): Configuration
    {
        return Configuration::forSymmetricSigner(
            $this->options->getSigner(),
            $this->options->RSASigner() ? $this->options->getPublicKey() : $this->options->getHamcKey()
        );
    }


    /**
     * Token是否失效
     * @param JwtToken $token
     * @return boolean
     */
    private function _isInvalid(JwtToken $token): bool
    {
        $jwtConfiguration = $this->getValidateConfig();

        $jwtConfiguration->setValidationConstraints(
            new SignedWith($jwtConfiguration->signer(), $jwtConfiguration->signingKey())
        );

        $constraints = $jwtConfiguration->validationConstraints();

        return !$jwtConfiguration->validator()->validate($token, ...$constraints);
    }

    /**
     * 效验Token是否过期
     * @param JwtToken $token
     * @return boolean
     */
    private function _isExpired(JwtToken $token): bool
    {
        $jwtConfiguration = $this->getValidateConfig();

        $jwtConfiguration->setValidationConstraints(
            new ValidAt(new SystemClock(new DateTimeZone(date_default_timezone_get()))),
        );

        $constraints = $jwtConfiguration->validationConstraints();

        return !$jwtConfiguration->validator()->validate($token, ...$constraints);
    }

    private function _fetchToken($handle)
    {
        $token = '';
        if($handle == 'Header') {
            $authorization = $this->app->request->header('authorization');
            if (!empty($authorization)) {
                if(str_starts_with($authorization, 'Bearer ')) {
                    $authorization = substr($authorization, 7);
                }
                return $authorization;
            }
        }

        if($handle == 'Url') {
            $token = $this->app->request->get('token');
            if(!empty($token)) return $token;
        }

        if($handle == 'Cookie') {
            $token = $this->app->cookie->get('token');
            if(!empty($token)) return $token;
        }

        if($handle == 'Post') {
            $token = $this->app->request->post('token');
            if(!empty($token)) return $token;
        }

        return $token;
    }

    private function _encryptToken(string $token): string
    {
        return strtoupper(bin2hex(openssl_encrypt($token, 'AES-256-ECB', $this->options->getAesKey(), OPENSSL_RAW_DATA)));
    }

    private function _decryptToken(string $token): bool|string
    {
        return openssl_decrypt(hex2bin($token), 'AES-256-ECB', $this->options->getAesKey(), OPENSSL_RAW_DATA);
    }
}
